<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>流式数据分段读取与断点续读</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
            color: #fff;
            min-height: 100vh;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        
        .container {
            width: 100%;
            max-width: 800px;
            background: rgba(0, 0, 0, 0.7);
            border-radius: 15px;
            padding: 25px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
            margin: 20px 0;
        }
        
        h1 {
            text-align: center;
            margin-bottom: 20px;
            color: #fdbb2d;
            font-size: 2.5rem;
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
        }
        
        h2 {
            margin: 25px 0 15px;
            color: #4dabf7;
            border-bottom: 2px solid #4dabf7;
            padding-bottom: 8px;
        }
        
        .section {
            margin-bottom: 25px;
            background: rgba(30, 30, 46, 0.6);
            padding: 20px;
            border-radius: 12px;
        }
        
        .controls {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            margin: 20px 0;
        }
        
        button {
            background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
            color: white;
            border: none;
            padding: 12px 25px;
            border-radius: 50px;
            cursor: pointer;
            font-size: 1rem;
            font-weight: 600;
            transition: all 0.3s ease;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
            flex: 1;
            min-width: 150px;
        }
        
        button:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
        }
        
        button:active {
            transform: translateY(1px);
        }
        
        button:disabled {
            background: linear-gradient(to right, #5c677d 0%, #7b879d 100%);
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }
        
        .btn-stop {
            background: linear-gradient(to right, #ff416c, #ff4b2b);
        }
        
        .btn-resume {
            background: linear-gradient(to right, #00b09b, #96c93d);
        }
        
        .progress-container {
            width: 100%;
            background: #333;
            border-radius: 10px;
            margin: 20px 0;
            overflow: hidden;
        }
        
        .progress-bar {
            height: 30px;
            background: linear-gradient(90deg, #00c6ff, #0072ff);
            border-radius: 10px;
            width: 0%;
            transition: width 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-weight: bold;
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
        }
        
        textarea {
            width: 100%;
            height: 150px;
            background: #222;
            color: #00ff9d;
            border: 2px solid #4dabf7;
            border-radius: 10px;
            padding: 15px;
            font-family: monospace;
            font-size: 14px;
            resize: vertical;
            margin: 15px 0;
        }
        
        .log {
            background: #111;
            border-radius: 10px;
            padding: 15px;
            height: 200px;
            overflow-y: auto;
            margin-top: 20px;
            font-family: monospace;
            font-size: 14px;
        }
        
        .log-entry {
            margin: 8px 0;
            padding: 8px;
            border-radius: 5px;
            background: rgba(255, 255, 255, 0.05);
        }
        
        .log-success {
            color: #00ff9d;
            border-left: 4px solid #00ff9d;
        }
        
        .log-warning {
            color: #ffcc00;
            border-left: 4px solid #ffcc00;
        }
        
        .log-error {
            color: #ff6b6b;
            border-left: 4px solid #ff6b6b;
        }
        
        .log-info {
            color: #4dabf7;
            border-left: 4px solid #4dabf7;
        }
        
        .status {
            display: flex;
            justify-content: space-between;
            margin: 15px 0;
            font-size: 1.1rem;
        }
        
        .status-item {
            background: rgba(77, 171, 247, 0.2);
            padding: 12px;
            border-radius: 8px;
            flex: 1;
            margin: 0 5px;
            text-align: center;
        }
        
        .chunk-size-control {
            display: flex;
            align-items: center;
            gap: 15px;
            margin: 15px 0;
        }
        
        input[type="range"] {
            flex: 1;
            height: 10px;
            -webkit-appearance: none;
            background: #333;
            border-radius: 5px;
            outline: none;
        }
        
        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 25px;
            height: 25px;
            border-radius: 50%;
            background: #4dabf7;
            cursor: pointer;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
        }
        
        .chunk-size-value {
            background: rgba(77, 171, 247, 0.3);
            padding: 8px 15px;
            border-radius: 20px;
            min-width: 100px;
            text-align: center;
        }
        
        .explanation {
            background: rgba(30, 30, 46, 0.6);
            padding: 20px;
            border-radius: 12px;
            margin-top: 25px;
        }
        
        .explanation h3 {
            color: #fdbb2d;
            margin-bottom: 15px;
        }
        
        .explanation ul {
            padding-left: 25px;
            margin: 15px 0;
        }
        
        .explanation li {
            margin: 10px 0;
            line-height: 1.6;
        }
        
        .code {
            background: #222;
            padding: 15px;
            border-radius: 8px;
            font-family: monospace;
            margin: 15px 0;
            color: #4dabf7;
            overflow-x: auto;
        }
        
        @media (max-width: 600px) {
            .controls {
                flex-direction: column;
            }
            
            .status {
                flex-direction: column;
                gap: 10px;
            }
        }
    </style>
</head>
<body>
    <h1>流式数据分段读取与断点续读</h1>
    
    <div class="container">
        <div class="section">
            <h2>数据源设置</h2>
            <textarea id="sourceData">这是一个演示流式数据分段读取的示例文本。流式处理允许我们处理大型数据集而无需一次性加载所有内容到内存中。通过分段读取和延迟加载，我们可以处理任意大小的数据，同时保持良好的性能表现。JavaScript中的ReadableStream API为这种处理模式提供了强大的支持。</textarea>
            <button id="createStream">创建可分段读取流</button>
        </div>
        
        <div class="section">
            <h2>读取控制</h2>
            <div class="chunk-size-control">
                <label for="chunkSize">读取块大小:</label>
                <input type="range" id="chunkSize" min="1" max="20" value="5">
                <div class="chunk-size-value"><span id="chunkSizeValue">5</span> 个字符</div>
            </div>
            
            <div class="controls">
                <button id="startReading" disabled>开始读取</button>
                <button id="pauseReading" class="btn-stop" disabled>暂停读取</button>
                <button id="resumeReading" class="btn-resume" disabled>继续读取</button>
                <button id="resetReader" disabled>重置读取器</button>
            </div>
            
            <div class="progress-container">
                <div class="progress-bar" id="progressBar">0%</div>
            </div>
            
            <div class="status">
                <div class="status-item">已读取: <span id="bytesRead">0</span> 字节</div>
                <div class="status-item">剩余: <span id="bytesRemaining">0</span> 字节</div>
                <div class="status-item">进度: <span id="progressPercent">0</span>%</div>
            </div>
        </div>
        
        <div class="section">
            <h2>读取结果</h2>
            <textarea id="result" placeholder="读取的数据将显示在这里..." readonly></textarea>
        </div>
        
        <div class="section">
            <h2>操作日志</h2>
            <div class="log" id="log"></div>
        </div>
    </div>
    
    <div class="container explanation">
        <h3>实现原理说明</h3>
        <p>本演示展示了如何实现流的分段读取和断点续读功能：</p>
        
        <ul>
            <li><strong>可重置读取器</strong>：读取器内部管理读取位置，重置时只需重新初始化读取器</li>
            <li><strong>流重用</strong>：无需重新创建流，只需重置读取器即可从头开始读取</li>
            <li><strong>pull 方法实现延迟加载</strong>：使用 pull 方法控制数据加载速度，每2秒加载一块数据</li>
            <li><strong>非阻塞UI</strong>：使用 requestAnimationFrame 确保UI响应性</li>
            <li><strong>缓冲区管理</strong>：保存读取位置和缓冲区，确保恢复时从正确位置继续</li>
        </ul>
        
        <div class="code">
// 核心代码片段 - 可重置读取器
class ResumableStreamReader {
    constructor(stream) {
        this.stream = stream;
        this.reader = null;
        this.position = 0;
        this.buffer = "";
        this.isReading = false;
    }
    
    // 初始化或重置读取器
    async init() {
        // 释放之前的读取器
        if (this.reader) {
            await this.reader.cancel();
            this.reader.releaseLock();
        }
        
        // 创建新的读取器
        this.reader = this.stream.getReader();
        this.position = 0;
        this.buffer = "";
        this.isReading = false;
    }
    
    // 读取下一块数据
    async readNextChunk(maxChars) {
        if (!this.isReading) return null;
        
        let chunk = '';
        let charsRead = 0;
        
        // 从缓冲区读取
        if (this.buffer) {
            const toTake = Math.min(maxChars, this.buffer.length);
            chunk = this.buffer.substring(0, toTake);
            this.buffer = this.buffer.substring(toTake);
            charsRead = chunk.length;
            this.position += chunk.length;
        }
        
        // 从流中读取更多数据
        while (charsRead < maxChars && this.isReading) {
            const { value, done } = await this.reader.read();
            if (done) break;
            
            // 计算需要读取的字符数
            const needed = maxChars - charsRead;
            const toTake = Math.min(needed, value.length);
            
            chunk += value.substring(0, toTake);
            charsRead += toTake;
            this.position += toTake;
            
            // 保存剩余部分到缓冲区
            if (toTake < value.length) {
                this.buffer = value.substring(toTake) + this.buffer;
            }
        }
        
        return chunk;
    }
    
    pause() {
        this.isReading = false;
    }
    
    resume() {
        this.isReading = true;
    }
    
    async reset() {
        this.pause();
        await this.init();
    }
    
    getPosition() {
        return this.position;
    }
}
        </div>
    </div>

    <script>
        class ResumableStreamReader {
            constructor(stream) {
                this.stream = stream;
                this.reader = null;
                this.position = 0;
                this.buffer = "";
                this.isReading = false;
            }
            
            // 初始化或重置读取器
            async init() {
                // 释放之前的读取器
                if (this.reader) {
                    await this.reader.cancel();
                    this.reader.releaseLock();
                }
                
                // 创建新的读取器
                this.reader = this.stream.getReader();
                this.position = 0;
                this.buffer = "";
                this.isReading = false;
            }
            
            // 读取下一块数据
            async readNextChunk(maxChars) {
                if (!this.isReading) return null;
                
                let chunk = '';
                let charsRead = 0;
                
                // 从缓冲区读取
                if (this.buffer) {
                    const toTake = Math.min(maxChars, this.buffer.length);
                    chunk = this.buffer.substring(0, toTake);
                    this.buffer = this.buffer.substring(toTake);
                    charsRead = chunk.length;
                    this.position += chunk.length;
                }
                
                // 从流中读取更多数据
                while (charsRead < maxChars && this.isReading) {
                    const { value, done } = await this.reader.read();
                    if (done) break;
                    
                    // 计算需要读取的字符数
                    const needed = maxChars - charsRead;
                    const toTake = Math.min(needed, value.length);
                    
                    chunk += value.substring(0, toTake);
                    charsRead += toTake;
                    this.position += toTake;
                    
                    // 保存剩余部分到缓冲区
                    if (toTake < value.length) {
                        this.buffer = value.substring(toTake) + this.buffer;
                    }
                }
                
                return chunk;
            }
            
            pause() {
                this.isReading = false;
            }
            
            resume() {
                this.isReading = true;
            }
            
            async reset() {
                this.pause();
                await this.init();
            }
            
            getPosition() {
                return this.position;
            }
        }
        
        // DOM元素
        const sourceDataEl = document.getElementById('sourceData');
        const resultEl = document.getElementById('result');
        const createStreamBtn = document.getElementById('createStream');
        const startReadingBtn = document.getElementById('startReading');
        const pauseReadingBtn = document.getElementById('pauseReading');
        const resumeReadingBtn = document.getElementById('resumeReading');
        const resetReaderBtn = document.getElementById('resetReader');
        const progressBarEl = document.getElementById('progressBar');
        const bytesReadEl = document.getElementById('bytesRead');
        const bytesRemainingEl = document.getElementById('bytesRemaining');
        const progressPercentEl = document.getElementById('progressPercent');
        const chunkSizeSlider = document.getElementById('chunkSize');
        const chunkSizeValue = document.getElementById('chunkSizeValue');
        const logEl = document.getElementById('log');
        
        // 全局变量
        let resumableReader = null;
        let stream = null;
        let totalBytes = 0;
        let isReadingActive = false;
        let animationFrameId = null;
        
        // 添加日志
        function addLog(message, type = 'success') {
            const logEntry = document.createElement('div');
            logEntry.className = `log-entry log-${type}`;
            logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
            logEl.appendChild(logEntry);
            logEl.scrollTop = logEl.scrollHeight;
        }
        
        // 更新进度
        function updateProgress() {
            if (!resumableReader || totalBytes === 0) return;
            
            const position = resumableReader.getPosition();
            const percent = Math.min(100, Math.round((position / totalBytes) * 100));
            
            progressBarEl.style.width = `${percent}%`;
            progressBarEl.textContent = `${percent}%`;
            bytesReadEl.textContent = position;
            bytesRemainingEl.textContent = totalBytes - position;
            progressPercentEl.textContent = percent;
        }
        
        // 创建带延迟加载的流
        function createResumableStream(text) {
            // 将文本分成小块
            const chunkSize = 5;
            let index = 0;
            const chunks = [];
            
            for (let i = 0; i < text.length; i += chunkSize) {
                chunks.push(text.substring(i, i + chunkSize));
            }
            
            return new ReadableStream({
                async pull(controller) {
                    // 添加2秒延迟模拟网络请求
                    await new Promise(resolve => setTimeout(resolve, 2000));
                    
                    if (index < chunks.length) {
                        addLog(`流推送新数据块 (${chunks[index].length} 字符)`, 'info');
                        controller.enqueue(chunks[index]);
                        index++;
                    } else {
                        controller.close();
                        addLog('流数据已全部推送完成', 'info');
                    }
                }
            });
        }
        
        // 异步读取函数
        async function readStream() {
            if (!resumableReader || !isReadingActive) return;
            
            const chunkSize = parseInt(chunkSizeSlider.value);
            
            try {
                const chunk = await resumableReader.readNextChunk(chunkSize);
                if (chunk) {
                    resultEl.value += chunk;
                    addLog(`读取 ${chunk.length} 字符 (缓冲区: ${resumableReader.buffer.length} 字符)`);
                    updateProgress();
                }
                
                // 检查是否完成
                if (resumableReader.getPosition() >= totalBytes) {
                    addLog('流读取完成!', 'success');
                    stopReading();
                    return;
                }
            } catch (error) {
                addLog(`读取错误: ${error.message}`, 'error');
                stopReading();
                return;
            }
            
            // 使用requestAnimationFrame继续读取，避免阻塞UI
            animationFrameId = requestAnimationFrame(readStream);
        }
        
        // 开始读取
        function startReading() {
            if (!resumableReader || resumableReader.getPosition() >= totalBytes) {
                return;
            }
            
            isReadingActive = true;
            resumableReader.resume();
            
            startReadingBtn.disabled = true;
            pauseReadingBtn.disabled = false;
            resumeReadingBtn.disabled = true;
            resetReaderBtn.disabled = false;
            
            addLog('开始读取流数据...');
            animationFrameId = requestAnimationFrame(readStream);
        }
        
        // 停止读取
        function stopReading() {
            isReadingActive = false;
            if (animationFrameId) {
                cancelAnimationFrame(animationFrameId);
                animationFrameId = null;
            }
            
            startReadingBtn.disabled = false;
            pauseReadingBtn.disabled = true;
            resumeReadingBtn.disabled = true;
        }
        
        // 事件监听器
        createStreamBtn.addEventListener('click', async () => {
            const text = sourceDataEl.value;
            if (!text.trim()) {
                addLog('错误: 请输入源数据', 'error');
                return;
            }
            
            stream = createResumableStream(text);
            resumableReader = new ResumableStreamReader(stream);
            await resumableReader.init();
            totalBytes = text.length;
            
            resultEl.value = '';
            progressBarEl.style.width = '0%';
            progressBarEl.textContent = '0%';
            bytesReadEl.textContent = '0';
            bytesRemainingEl.textContent = totalBytes;
            progressPercentEl.textContent = '0';
            
            startReadingBtn.disabled = false;
            resetReaderBtn.disabled = false;
            
            addLog(`已创建流，总大小: ${totalBytes} 字节`);
            addLog('流将每2秒推送一个新数据块', 'info');
            updateProgress();
        });
        
        startReadingBtn.addEventListener('click', startReading);
        
        pauseReadingBtn.addEventListener('click', () => {
            if (resumableReader && isReadingActive) {
                resumableReader.pause();
                stopReading();
                addLog('读取已暂停');
            }
        });
        
        resumeReadingBtn.addEventListener('click', () => {
            if (resumableReader) {
                addLog('继续读取...');
                startReading();
            }
        });
        
        resetReaderBtn.addEventListener('click', async () => {
            if (resumableReader) {
                stopReading();
                await resumableReader.reset();
                
                resultEl.value = '';
                progressBarEl.style.width = '0%';
                progressBarEl.textContent = '0%';
                bytesReadEl.textContent = '0';
                bytesRemainingEl.textContent = totalBytes;
                progressPercentEl.textContent = '0';
                
                startReadingBtn.disabled = false;
                pauseReadingBtn.disabled = true;
                resumeReadingBtn.disabled = true;
                
                addLog('读取器已重置，准备重新开始读取');
            }
        });
        
        chunkSizeSlider.addEventListener('input', () => {
            const size = parseInt(chunkSizeSlider.value);
            chunkSizeValue.textContent = size;
        });
        
        // 初始化
        chunkSizeValue.textContent = chunkSizeSlider.value;
        addLog('准备好创建流并开始分段读取', 'success');
    </script>
</body>
</html>